operator
delete
if you
write
operator
new
.operator
new
or operator
delete
in the first
place?
More often than not, the answer is efficiency. The default versions of
operator
new
and operator
delete
are perfectly adequate for general-purpose use, but
their flexibility inevitably leaves room for improvements in their
performance in a more circumscribed context. This is especially true for
applications that dynamically allocate a large number of small objects.
As an example, consider a class for representing airplanes, where the
Airplane
class contains only a pointer to the actual
representation for airplane objects (a technique discussed in Item 34):
class AirplaneRep { ... }; // representation for an // Airplane object class Airplane { public: ... private: AirplaneRep *rep; // pointer to representation };
Airplane
object is not very big; it contains but a single
pointer. (As explained in Item 14, it may implicitly contain a second
pointer if the Airplane
class declares virtual functions.)
When you allocate an Airplane
object by calling
operator
new
, however, you probably get back much
more memory than is needed to store this pointer (or pair of pointers). The
reason for this seemingly wayward behavior has to do with the need for
operator
new
and operator
delete
to communicate with one another.
Because the default version of operator
new
is a
general-purpose allocator, it must be prepared to allocate blocks of
any size. Similarly, the default version of operator
delete
must be prepared to deallocate blocks of whatever size
operator
new
allocated. For operator
delete
to know how much memory to deallocate, it must have
some way of knowing how much memory operator
new
allocated in the first place. A common way for operator
new
to tell operator
delete
how much
memory it allocated is by prepending to the memory it returns some
additional data that specifies the size of the allocated block. That is,
when you say this,
Airplane *pa = new Airplane;
Instead, you often get back a block of memory that looks more like this:
For small objects like those of class Airplane
, this
additional bookkeeping data can more than double the amount of memory
needed for each dynamically allocated object (especially if the class
contains no virtual functions).
If you're developing software for an environment in which memory is
precious, you may not be able to afford this kind of spendthrift
allocation. By writing your own operator
new
for
the Airplane
class, you can take advantage of the fact that
all Airplane
objects are the same size, so there isn't
any need for bookkeeping information to be kept with each allocated
block.
One way to implement your class-specific operator
new
is to ask the default operator
new
for big blocks of raw memory, each block of sufficient
size to hold a large number of Airplane
objects. The memory
chunks for Airplane
objects themselves will be taken from
these big blocks. Currently unused chunks will be organized into a linked
list -- the free list -- of chunks that are available for future
Airplane
use. This may make it sound like you'll have to
pay for the overhead of a next
field in every object (to
support the list), but you won't: the space for the rep
field (which is necessary only for memory chunks in use as
Airplane
objects) will also serve as the place to store the
next
pointer (because that pointer is needed only for chunks
of memory not in use as Airplane
objects).
You'll arrange for this job-sharing in the usual fashion: you'll
use a union
.
To turn this design into reality, you have to modify the definition of
Airplane
to support custom memory management. You do it as
follows:
class Airplane { // modified class -- now supports public: // custom memory management static void * operator new(size_t size); ... private: union { AirplaneRep *rep; // for objects in use Airplane *next; // for objects on free list }; // this class-specific constant (see Item 1) specifies how // many Airplane objects fit into a big memory block; // it's initialized below static const int BLOCK_SIZE; static Airplane *headOfFreeList; };
operator
new
, the union that allows the rep
and
next
fields to occupy the same memory, a class-specific
constant for specifying how big each allocated block should be, and a
static pointer to keep track of the head of the free list. It's
important to use a static member for this last task, because there's
one free list for the entire class, not one free list for each
Airplane
object.
The next thing to do is to write the new operator
new
:
void * Airplane::operator new(size_t size) { // send requests of the "wrong" size to ::operator new(); // for details, see Item 8 if (size != sizeof(Airplane)) return ::operator new(size); Airplane *p = // p is now a pointer to the headOfFreeList; // head of the free list // if p is valid, just move the list head to the // next element in the free list if (p) headOfFreeList = p->next; else { // The free list is empty. Allocate a block of memory // big enough hold BLOCK_SIZE Airplane objects Airplane *newBlock = static_cast<Airplane*>(::operator new(BLOCK_SIZE * sizeof(Airplane))); // form a new free list by linking the memory chunks // together; skip the zeroth element, because you'll // return that to the caller of operator new for (int i = 1; i < BLOCK_SIZE-1; ++i) newBlock[i].next = &newBlock[i+1]; // terminate the linked list with a null pointer newBlock[BLOCK_SIZE-1].next = 0; // set p to front of list, headOfFreeList to // chunk immediately following p = newBlock; headOfFreeList = &newBlock[1]; } return p; }
operator
new
can't satisfy a request for memory, it's
supposed to perform a series of ritualistic steps involving new-handler
functions and exceptions. There is no sign of such steps above. That's
because this operator
new
gets all the memory it
manages from ::operator
new
. That means this
operator
new
can fail only if
::operator
new
does. But if
::operator
new
fails, it must engage in
the new-handling ritual (possibly culminating in the throwing of an
exception), so there is no need for Airplane
's
operator
new
to do it, too. In other words, the
new-handler behavior is there, you just don't see it, because
it's hidden inside ::operator
new
.
Given this operator
new
, the only thing left to
do is provide the obligatory definitions of Airplane
's
static data members:
Airplane *Airplane::headOfFreeList; // these definitions // go in an implemen- const int Airplane::BLOCK_SIZE = 512; // tation file, not // a header file
headOfFreeList
to the
null pointer, because static members are initialized to 0 by default. The
value for BLOCK_SIZE
, of course, determines the size of each
memory block we get from ::operator
new
.
This version of operator
new
will work just
fine. Not only will it use a lot less memory for Airplane
objects than the default operator
new
, it's
also likely to be faster, possibly as much as two orders of magnitude
faster. That shouldn't be surprising. After all, the general
version of operator
new
has to cope with memory
requests of different sizes, has to worry about internal and external
fragmentation, etc., whereas your version of operator
new
just manipulates a couple of pointers in a linked list.
It's easy to be fast when you don't have to be flexible.
At long last we are in a position to discuss operator
delete
. Remember operator
delete
?
This Item is about operator
delete
. As
currently written, your Airplane
class declares
operator
new
, but it does not declare
operator
delete
. Now consider what happens when a
client writes the following, which is nothing if not eminently
reasonable:
Airplane *pa = new Airplane; // calls // Airplane::operator new ... delete pa; // calls ::operator delete
operator
new
(the one defined in Airplane
) returns a
pointer to memory without any header information, but
operator
delete
(the default, global one) assumes
that the memory it's passed does contain header information!
Surely this is a recipe for disaster.
This example illustrates the general rule: operator
new
and operator
delete
must be
written in concert so that they share the same assumptions. If you're
going to roll your own memory allocation routine, be sure to roll one for
deallocation, too.
Here's how you solve the problem with the Airplane
class:
class Airplane { // same as before, except there's public: // now a decl. for operator delete ... static void operator delete(void *deadObject, size_t size); }; // operator delete is passed a memory chunk, which, // if it's the right size, is just added to the // front of the list of free chunks void Airplane::operator delete(void *deadObject, size_t size) { if (deadObject == 0) return; // see Item 8 if (size != sizeof(Airplane)) { // see Item 8 ::operator delete(deadObject); return; } Airplane *carcass = static_cast<Airplane*>(deadObject); carcass->next = headOfFreeList; headOfFreeList = carcass; }
operator
new
to
ensure that calls of the "wrong" size were forwarded to the
global operator
new
(see Item 8), you must
demonstrate equal care in ensuring that such "improperly sized"
objects are handled by the global version of operator
delete
. If you did not, you'd run into precisely the
problem you have been laboring so arduously to avoid -- a semantic mismatch
between new
and delete
.
Interestingly, the size_t
value C++ passes to
operator
delete
may be incorrect if the
object being deleted was derived from a base class lacking a virtual
destructor. This is reason enough for making sure your base classes have
virtual destructors, but Item 14 describes a second, arguably better
reason. For now, simply note that if you omit virtual destructors in base
classes, operator
delete
functions may not work
correctly.
All of which is well and good, but I can tell by the furrow in your brow
that what you're really concerned about is the memory leak. With all
the software development experience you bring to the table, there's no
way you'd fail to notice that Airplane
's
operator
new
calls ::operator
new
to get big blocks of memory, but Airplane's
operator
delete
fails to release those
blocks.Memory leak! Memory leak! I can almost hear the alarm bells
going off in your head.
Listen to me carefully: there is no memory leak.
A memory leak arises when memory is allocated, then all pointers to that
memory are lost. Absent garbage collection or some other extralinguistic
mechanism, such memory cannot be reclaimed. But this design has no memory
leak, because it's never the case that all pointers to memory are
lost. Each big block of memory is first broken down into
Airplane
-sized chunks, and these chunks are then placed on the
free list. When clients call Airplane::operator
new
, chunks are removed from the free list, and clients
receive pointers to them. When clients call operator
delete
, the chunks are put back on the free list. With this
design, all memory chunks are either in use as Airplane
objects (in which case it's the clients' responsibility to avoid
leaking their memory) or are on the free list (in which case there's a
pointer to the memory). There is no memory leak.
Nevertheless, the blocks of memory returned by ::operator
new
are never released by Airplane::operator
delete
, and there has to be some name for that. There is.
You've created a memory pool. Call it semantic gymnastics if you must,
but there is an important difference between a memory leak and a memory
pool. A memory leak may grow indefinitely, even if clients are
well-behaved, but a memory pool never grows larger than the maximum amount
of memory requested by its clients.
It would not be difficult to modify Airplane
's memory
management routines so that the blocks of memory returned by
::operator
new
were automatically released when
they were no longer in use, but there are two reasons why you might not
want to do it.
The first concerns your likely motivation for tackling custom memory
management. There are many reasons why you might do it, but the most common
one is that you've determined that the default operator
new
and operator
delete
use too much
memory or are too slow (or both). That being the case, every additional
byte and every additional statement you devote to tracking and releasing
those big memory blocks comes straight off the bottom line: your software
runs slower and uses more memory than it would if you adopted the pool
strategy. For libraries and applications in which performance is at a
premium and you can expect pool sizes to be reasonably bounded, the pool
approach may well be best.
The second reason has to do with pathological behavior. Suppose
Airplane
's memory management routines are modified so
Airplane
's operator
delete
releases any big block of memory that has no active objects in it. Now
consider this program:
int main() { Airplane *pa = new Airplane; // first allocation: get big // block, make free list, etc. delete pa; // block is now empty; // release it pa = new Airplane; // uh oh, get block again, // make free list, etc. delete pa; // okay, block is empty // again; release it ... // you get the idea... return 0; }
operator
new
and
operator
delete
, much less the pool-based
versions of those functions!Of course, there are ways to deal with this pathology, but the more you code for uncommon special cases, the closer you get to reimplementing the default memory management functions, and then what have you gained? A memory pool is not the answer to all memory management questions, but it's a reasonable answer to many of them.
In fact, it's a reasonable answer often enough that you may be bothered by the need to reimplement it for different classes. "Surely," you think to yourself, "there should be a way to package the notion of a fixed-sized memory allocator so it's easily reused." There is, though this Item has droned on long enough that I'll leave the details in the form of the dreaded exercise for the reader.
Instead, I'll simply show a minimal interface (see Item 18) to a
Pool
class, where each object of type Pool
is an
allocator for objects of the size specified in the Pool
's
constructor:
class Pool { public: Pool(size_t n); // Create an allocator for // objects of size n void * alloc(size_t n); // Allocate enough memory // for one object; follow // operator new conventions // from Item 8 void free(void *p, size_t n); // Return to the pool the // memory pointed to by p; // follow operator delete // conventions from Item 8 ~Pool(); // Deallocate all memory in // the pool };
Pool
objects to be created, to perform
allocation and deallocation operations, and to be destroyed. When a
Pool
object is destroyed, it releases all the memory it
allocated. This means there is now a way to avoid the memory leak-like
behavior that Airplane
's functions exhibited. However,
this also means that if a Pool
's destructor is called too
soon (before all the objects using its memory have been destroyed), some
objects will find their memory yanked out from under them before
they're done using it. To say that the resulting behavior is undefined
is being generous.
Given this Pool
class, even a Java programmer can add custom
memory management capabilities to Airplane
without breaking a
sweat:
class Airplane { public: ... // usual Airplane functions static void * operator new(size_t size); static void operator delete(void *p, size_t size); private: AirplaneRep *rep; // pointer to representation static Pool memPool; // memory pool for Airplanes }; inline void * Airplane::operator new(size_t size) { return memPool.alloc(size); } inline void Airplane::operator delete(void *p, size_t size) { memPool.free(p, size); } // create a new pool for Airplane objects; this goes in // the class implementation file Pool Airplane::memPool(sizeof(Airplane));
Airplane
class is no longer cluttered with non-airplane
details. Gone is the union
, the head of the free list, the
constant defining how big each raw memory block should be, etc. That's
all hidden inside Pool
, which is really where it should be.
Let Pool
's author worry about memory management minutiae.
Your job is to make the Airplane
class work properly.
Now, it's interesting to see how custom memory management routines
can improve program performance, and it's worthwhile to see how such
routines can be encapsulated inside a class like Pool
, but let
us not lose sight of the main point. That point is that
operator
new
and operator
delete
need to work together, so if you write
operator
new
, be sure to write
operator
delete
, as well.